package org.skydiveapplibrary.util;

import java.io.IOException;
import java.io.Writer;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;

import org.skydiveapplibrary.R;
import org.skydiveapplibrary.data.Aircraft;
import org.skydiveapplibrary.data.AltitudeUnit;
import org.skydiveapplibrary.data.Database;
import org.skydiveapplibrary.data.Location;
import org.skydiveapplibrary.data.LogbookHistory;
import org.skydiveapplibrary.data.Rig;
import org.skydiveapplibrary.data.RigComponent;
import org.skydiveapplibrary.data.RigReminder;
import org.skydiveapplibrary.data.Signature;
import org.skydiveapplibrary.data.SkydiveType;
import org.skydiveapplibrary.repository.AircraftRepository;
import org.skydiveapplibrary.repository.LocationRepository;
import org.skydiveapplibrary.repository.LogEntryRepository;
import org.skydiveapplibrary.repository.LogbookHistoryRepository;
import org.skydiveapplibrary.repository.RigRepository;
import org.skydiveapplibrary.repository.SignatureRepository;
import org.skydiveapplibrary.repository.SkydiveTypeRepository;
import org.skydiveapplibrary.ui.FormatUtility;
import org.xmlpull.v1.XmlSerializer;

import android.content.Context;
import android.database.Cursor;
import android.util.Base64;
import android.util.Xml;

public class DatabaseExportUtil
{
	private static final String NEW_LINE = "\n";
	private static final String INDENT = "  ";
	private static final String NAMESPACE = "http://www.skydiving-logbook.org/logbook";
	
	// context
	private Context context;
	
	// maps
	private Map<String, SkydiveType> skydiveTypeMap = new HashMap<String, SkydiveType>();
	private Map<String, Location> locationMap = new HashMap<String, Location>();
	private Map<String, Aircraft> aircraftMap = new HashMap<String, Aircraft>();
	private Map<String, Rig> rigMap = new HashMap<String, Rig>();
	
	// date format
	private DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
	private DateFormat dateTimeFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
	
	public DatabaseExportUtil(Context context)
	{
		this.context = context;
	}
	
	public void exportDatabase(Writer xmlWriter, Writer csvWriter)
		throws IOException
	{
		initializeData();
		XmlSerializer xmlSerializer = initializeXml(xmlWriter);
		CsvSerializer csvSerializer = initializeCsv(csvWriter);

		// set timezone for date/time format
		dateTimeFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
		
		// start
		startTagWithAttribute(xmlSerializer, "skydiving_logbook", "version", "2", 0);
		
		// add logbook history
		addLogbookHistory(xmlSerializer);
		// log entries
		addLogEntries(xmlSerializer, csvSerializer);
		// signatures
		addSignatures(xmlSerializer);
		// skydive types
		addSkydiveTypes(xmlSerializer);
		// locations
		addLocations(xmlSerializer);
		// aircrafts
		addAircrafts(xmlSerializer);
		// rigs
		addRigs(xmlSerializer);
		
		endTag(xmlSerializer, "skydiving_logbook", 0);
		xmlSerializer.endDocument();
	}
	
	private void initializeData()
	{
		// get skydive types
		SkydiveTypeRepository skydiveRepository = new SkydiveTypeRepository(context);
		for (SkydiveType skydiveType: skydiveRepository.getAllSkydiveTypes())
		{
			skydiveTypeMap.put(skydiveType.getUniqueId(), skydiveType);
		}
		// get aircrafts
		AircraftRepository aircraftRepository = new AircraftRepository(context);
		for (Aircraft aircraft: aircraftRepository.getAllAircrafts())
		{
			aircraftMap.put(aircraft.getUniqueId(), aircraft);
		}
		// get locations
		LocationRepository locationRepository = new LocationRepository(context);
		for (Location location: locationRepository.getAllLocations())
		{
			locationMap.put(location.getUniqueId(), location);
		}
		// get rigs
		RigRepository rigRepository = new RigRepository(context);
		for (Rig rig: rigRepository.getAllRigs())
		{
			rigMap.put(rig.getUniqueId(), rig);
		}
	}
	
	private XmlSerializer initializeXml(Writer xmlWriter)
		throws IOException
	{
		XmlSerializer serializer = Xml.newSerializer();
		serializer.setOutput(xmlWriter);
		serializer.startDocument("UTF-8", true);
		serializer.ignorableWhitespace(NEW_LINE);
		serializer.setPrefix("", NAMESPACE);
		
		return serializer;
	}
	
	private CsvSerializer initializeCsv(Writer csvWriter)
		throws IOException
	{
		CsvSerializer serializer = new CsvSerializer(csvWriter);
		
		// write csv heder
		serializer.addRow(new String[]
		{
			context.getString(R.string.import_export_jump_number_header),
			context.getString(R.string.import_export_date_header),
			context.getString(R.string.import_export_location_header),
			context.getString(R.string.import_export_aircraft_header),
			context.getString(R.string.import_export_gear_header),
			context.getString(R.string.import_export_skydive_type_header),
			context.getString(R.string.import_export_exit_alt_header),
			context.getString(R.string.import_export_depl_alt_header),
			context.getString(R.string.import_export_alt_unit_header),
			context.getString(R.string.import_export_freefall_time_header),
			context.getString(R.string.import_export_cutaway_header),
			context.getString(R.string.import_export_notes_header),
		});
		
		return serializer;
	}
	
	private void addLogbookHistory(XmlSerializer serializer)
		throws IOException
	{
		// get history
		LogbookHistoryRepository repository = new LogbookHistoryRepository(context);
		LogbookHistory history = repository.getHistory();
		
		startTag(serializer, "logbook_history", 1);
		writeIntValue(serializer, "freefall_time", history.getFreefallTime(), 2);
		writeIntValue(serializer, "cutaways", history.getCutaways(), 2);
		endTag(serializer, "logbook_history", 1);
	}
	
	private void addLogEntries(XmlSerializer xmlSerializer, CsvSerializer csvSerializer)
		throws IOException
	{
		Cursor cursor = null;
		try
		{
			// repo
			LogEntryRepository repository = new LogEntryRepository(context);

			// get log entry rigs
			Map<Integer, List<String>> logEntryRigs = new HashMap<Integer, List<String>>();
			cursor =  repository.getAllLogEntryRigsForExportCursor();
			while (cursor.moveToNext())
			{
				int logEntryId = Database.getInteger(cursor, Database.LOG_ENTRY_RIG_LOG_ENTRY);
				String rigId = Database.getString(cursor, Database.LOG_ENTRY_RIG_RIG);
				if (!logEntryRigs.containsKey(logEntryId))
					logEntryRigs.put(logEntryId, new ArrayList<String>());
				logEntryRigs.get(logEntryId).add(rigId);
			}
			cursor.close();
			
			// get log entry signatures
			Map<Integer, Integer> logEntrySignatures = new HashMap<Integer, Integer>();
			cursor = repository.getAllLogEntrySignaturesCursor();
			while (cursor.moveToNext())
			{
				int logEntryId = Database.getInteger(cursor, Database.LOG_ENTRY_SIGNATURE_LOG_ENTRY);
				int signatureId = Database.getInteger(cursor, Database.LOG_ENTRY_SIGNATURE_SIGNATURE);
				logEntrySignatures.put(logEntryId, signatureId);
			}
			cursor.close();

			// get log entries
			cursor = repository.getAllLogEntryForExportCursor();
			
			// start tag
			startTag(xmlSerializer, "log_entries", 1);
			while (cursor.moveToNext())
			{
				// get rig ids
				int logEntryId = Database.getInteger(cursor, Database.LOG_ENTRY_ID);
				List<String> rigIds = logEntryRigs.containsKey(logEntryId) ? logEntryRigs.get(logEntryId) :
					new ArrayList<String>();
				Integer signatureId = logEntrySignatures.containsKey(logEntryId) ?
						logEntrySignatures.get(logEntryId) : null;
				writeLogEntryToXml(cursor, rigIds, signatureId, xmlSerializer);
				writeLogEntryToCsv(cursor, rigIds, csvSerializer);
			}
			endTag(xmlSerializer, "log_entries", 1);
		}
		finally
		{
			if (cursor != null)
			{
				cursor.close();
			}
		}
	}
	
	private void writeLogEntryToXml(
			Cursor cursor,
			List<String> rigIds,
			Integer signatureId,
			XmlSerializer xmlSerializer)
		throws IOException
	{
		startTag(xmlSerializer, "log_entry", 2);
		writeValue(xmlSerializer, "id", Database.getString(cursor, Database.LOG_ENTRY_UNIQUE_ID), 3);
		writeIntValue(xmlSerializer, "jump_number", Database.getInteger(cursor, Database.LOG_ENTRY_JUMP_NUMBER), 3);
		writeDateValue(xmlSerializer, "date", Database.getDate(cursor, Database.LOG_ENTRY_DATE), 3);
		writeValue(xmlSerializer, "location_id", Database.getString(cursor, Database.LOG_ENTRY_LOCATION), 3);
		writeValue(xmlSerializer, "aircraft_id", Database.getString(cursor, Database.LOG_ENTRY_AIRCRAFT), 3);
		// rigs
		startTag(xmlSerializer, "rigs", 3);
		for (String rigId: rigIds)
		{
			writeValue(xmlSerializer, "rig_id", rigId, 4);
		}
		endTag(xmlSerializer, "rigs", 3);
		writeValue(xmlSerializer, "skydive_type_id", Database.getString(cursor, Database.LOG_ENTRY_SKYDIVE_TYPE), 3);
		writeIntValue(xmlSerializer, "exit_altitude", Database.getInteger(cursor, Database.LOG_ENTRY_EXIT_ALTITUDE), 3);
		writeIntValue(xmlSerializer, "deployment_altitude", Database.getInteger(cursor, Database.LOG_ENTRY_DEPLOYMENT_ALTITUDE), 3);
		writeValue(xmlSerializer, "altitude_unit", Database.getString(cursor, Database.LOG_ENTRY_ALTITUDE_UNIT), 3);
		writeIntValue(xmlSerializer, "freefall_time", Database.getInteger(cursor, Database.LOG_ENTRY_FREEFALL_TIME), 3);
		writeBooleanValue(xmlSerializer, "cutaway", Database.getBoolean(cursor, Database.LOG_ENTRY_CUTAWAY), 3);
		writeValue(xmlSerializer, "notes", Database.getString(cursor, Database.LOG_ENTRY_NOTES), 3);
		writeImageValue(xmlSerializer, "diagram", Database.getBinaryData(cursor, Database.LOG_ENTRY_DIAGRAM), 3);
		if (signatureId != null)
			writeIntValue(xmlSerializer, "signature_id", signatureId, 3);
		writeDateTimeValue(xmlSerializer, "last_modified_utc", Database.getDateTime(cursor, Database.LOG_ENTRY_LAST_MODIFIED_UTC), 3);
		if (signatureId != null)
			writeDateTimeValue(xmlSerializer, "last_signature_utc", Database.getDateTime(cursor, Database.LOG_ENTRY_LAST_SIGNATURE_UTC), 3);

		endTag(xmlSerializer, "log_entry", 2);
	}
	
	private void writeLogEntryToCsv(
			Cursor cursor,
			List<String> rigIds,
			CsvSerializer csvSerializer)
		throws IOException
	{
		// get location name
		String locationId = Database.getString(cursor, Database.LOG_ENTRY_LOCATION);
		String locationName = locationId == null ? "" :
			locationMap.get(locationId).getName();
		// get skydive type
		String skydiveTypeId = Database.getString(cursor, Database.LOG_ENTRY_SKYDIVE_TYPE);
		String skydiveTypeName = skydiveTypeId == null ? "" :
			skydiveTypeMap.get(skydiveTypeId).getName();
		// get aircraft name
		String aircraftId = Database.getString(cursor, Database.LOG_ENTRY_AIRCRAFT);
		String aircraftName = aircraftId == null ? "" :
			aircraftMap.get(aircraftId).getName();
		// get rig names
		String rigNames = getRigsString(rigIds);
		
		csvSerializer.addRow(new String[]
        {
				CsvSerializer.formatInteger(Database.getInteger(cursor, Database.LOG_ENTRY_JUMP_NUMBER)),
				CsvSerializer.formatDate(Database.getDate(cursor, Database.LOG_ENTRY_DATE)),
				CsvSerializer.formatString(locationName),
				CsvSerializer.formatString(aircraftName),
				CsvSerializer.formatString(rigNames),
				CsvSerializer.formatString(skydiveTypeName),
				CsvSerializer.formatInteger(Database.getInteger(cursor, Database.LOG_ENTRY_EXIT_ALTITUDE)),
				CsvSerializer.formatInteger(Database.getInteger(cursor, Database.LOG_ENTRY_DEPLOYMENT_ALTITUDE)),
				context.getString(AltitudeUnit.valueOf(Database.getString(cursor, Database.LOG_ENTRY_ALTITUDE_UNIT)).getResourceId()),
				CsvSerializer.formatInteger(Database.getInteger(cursor, Database.LOG_ENTRY_FREEFALL_TIME)),
				CsvSerializer.formatBoolean(Database.getBoolean(cursor, Database.LOG_ENTRY_CUTAWAY),
						context.getString(R.string.import_export_yes),
						context.getString(R.string.import_export_no)),
				CsvSerializer.formatString(Database.getString(cursor, Database.LOG_ENTRY_NOTES))
		});
	}
	
	private String getRigsString(List<String> rigIds)
	{
		// set gear text
		StringBuilder builder = new StringBuilder();
		for (String rigId: rigIds)
		{
			builder.append(rigMap.get(rigId).getName());
			// if not last, append ", "
			if (rigId != rigIds.get(rigIds.size() - 1))
				builder.append(", ");
		}
		return builder.toString();
	}
	
	private void addSignatures(XmlSerializer serializer)
		throws IOException
	{
		// get signatures
		SignatureRepository repository = new SignatureRepository(context);
		
		// start
		startTag(serializer, "signatures", 1);
		for (Signature signature: repository.getSignatures())
		{
			startTag(serializer, "signature", 2);
			writeIntValue(serializer, "id", signature.getId(), 3);
			writeValue(serializer, "license", signature.getLicense(), 3);
			writeImageValue(serializer, "image", signature.getImage(), 3);
			endTag(serializer, "signature", 2);
		}
		// end
		endTag(serializer, "signatures", 1);
	}
	
	private void addSkydiveTypes(XmlSerializer serializer)
		throws IOException
	{
		// start
		startTag(serializer, "skydive_types", 1);
		for (SkydiveType skydiveType: skydiveTypeMap.values())
		{
			startTag(serializer, "skydive_type", 2);
			writeValue(serializer, "id", skydiveType.getUniqueId(), 3);
			writeValue(serializer, "name", skydiveType.getName(), 3);
			writeValue(serializer, "freefall_profile", skydiveType.getFreefallProfileType() == null ? null :
				skydiveType.getFreefallProfileType().toString(), 3);
			writeValue(serializer, "notes", skydiveType.getNotes(), 3);
			writeBooleanValue(serializer, "active", skydiveType.isActive(), 3);
			writeDateTimeValue(serializer, "last_modified_utc", skydiveType.getLastModifiedUtc(), 3);
			endTag(serializer, "skydive_type", 2);
		}
		// end
		endTag(serializer, "skydive_types", 1);
	}
	
	private void addLocations(XmlSerializer serializer)
		throws IOException
	{
		// start
		startTag(serializer, "locations", 1);
		for (Location location: locationMap.values())
		{
			startTag(serializer, "location", 2);
			writeValue(serializer, "id", location.getUniqueId(), 3);
			writeValue(serializer, "name", location.getName(), 3);
			writeBooleanValue(serializer, "home", location.isHome(), 3);
			writeValue(serializer, "notes", location.getNotes(), 3);
			writeBooleanValue(serializer, "active", location.isActive(), 3);
			writeDateTimeValue(serializer, "last_modified_utc", location.getLastModifiedUtc(), 3);
			endTag(serializer, "location", 2);
		}
		// end
		endTag(serializer, "locations", 1);
	}
	
	private void addAircrafts(XmlSerializer serializer)
		throws IOException
	{
		// start
		startTag(serializer, "aircrafts", 1);
		for (Aircraft aircraft: aircraftMap.values())
		{
			startTag(serializer, "aircraft", 2);
			writeValue(serializer, "id", aircraft.getUniqueId(), 3);
			writeValue(serializer, "name", aircraft.getName(), 3);
			writeValue(serializer, "notes", aircraft.getNotes(), 3);
			writeBooleanValue(serializer, "active", aircraft.isActive(), 3);
			writeDateTimeValue(serializer, "last_modified_utc", aircraft.getLastModifiedUtc(), 3);
			endTag(serializer, "aircraft", 2);
		}
		// end
		endTag(serializer, "aircrafts", 1);
	}
	
	private void addRigs(XmlSerializer serializer)
		throws IOException
	{
		// start
		startTag(serializer, "rigs", 1);
		for (Rig rig: rigMap.values())
		{
			startTag(serializer, "rig", 2);
			writeValue(serializer, "id", rig.getUniqueId(), 3);
			writeValue(serializer, "name", rig.getName(), 3);
			writeBooleanValue(serializer, "primary", rig.isPrimary(), 3);
			writeBooleanValue(serializer, "archived", rig.isArchived(), 3);
			writeValue(serializer, "notes", rig.getNotes(), 3);
			writeBooleanValue(serializer, "active", rig.isActive(), 3);
			writeDateTimeValue(serializer, "last_modified_utc", rig.getLastModifiedUtc(), 3);
			
			// components
			startTag(serializer, "components", 3);
			for (RigComponent component: rig.getComponents())
			{
				startTag(serializer, "component", 4);
				writeValue(serializer, "name", component.getName(), 5);
				writeValue(serializer, "serial_number", component.getSerialNumber(), 5);
				writeValue(serializer, "notes", component.getNotes(), 5);
				endTag(serializer, "component", 4);
			}
			endTag(serializer, "components", 3);
			
			// reminders
			startTag(serializer, "reminders", 3);
			for (RigReminder reminder: rig.getReminders())
			{
				startTag(serializer, "reminder", 4);
				writeValue(serializer, "name", reminder.getName(), 5);
				writeIntValue(serializer, "interval", reminder.getInterval(), 5);
				writeValue(serializer, "interval_unit", reminder.getIntervalUnit().toString(), 5);
				writeDateValue(serializer, "last_completed_date", reminder.getLastCompletedDate(), 5);
				endTag(serializer, "reminder", 4);
			}
			endTag(serializer, "reminders", 3);			
			endTag(serializer, "rig", 2);
		}
		// end
		endTag(serializer, "rigs", 1);
	}
	
	private void startTag(XmlSerializer serializer, String tagName, int indent)
		throws IOException
	{
		indent(serializer, indent);
		serializer.startTag(NAMESPACE, tagName);
		serializer.ignorableWhitespace(NEW_LINE);
	}
	
	private void startTagWithAttribute(XmlSerializer serializer, String tagName, String attributeName, String attributeValue, int indent)
		throws IOException
	{
		indent(serializer, indent);
		serializer.startTag(NAMESPACE, tagName);
		serializer.attribute("", attributeName, attributeValue);
		serializer.ignorableWhitespace(NEW_LINE);
	}
	
	private void endTag(XmlSerializer serializer, String tagName, int indent)
		throws IOException
	{
		indent(serializer, indent);
		serializer.endTag(NAMESPACE, tagName);
		serializer.ignorableWhitespace(NEW_LINE);
	}
	
	private void writeIntValue(XmlSerializer serializer, String tagName, Integer value, int indent)
		throws IOException
	{
		writeValue(serializer, tagName, value == null ? null : String.valueOf(value), indent);
	}
	
	private void writeBooleanValue(XmlSerializer serializer, String tagName, Boolean value, int indent)
		throws IOException
	{
		writeValue(serializer, tagName, value == null ? null : String.valueOf(value), indent);
	}

	private void writeDateValue(XmlSerializer serializer, String tagName, Date value, int indent)
		throws IOException
	{
		writeValue(serializer, tagName, value == null ? null : dateFormat.format(value), indent);
	}
	
	private void writeDateTimeValue(XmlSerializer serializer, String tagName, Date value, int indent)	
		throws IOException
	{
		writeValue(serializer, tagName, value == null ? null : dateTimeFormat.format(value), indent);
	}
	
	private void writeImageValue(XmlSerializer serializer, String tagName, byte[] value, int indent)
		throws IOException
	{
		writeValue(serializer, tagName, value == null ? null : Base64.encodeToString(value, Base64.DEFAULT), indent);
	}

	private void writeValue(XmlSerializer serializer, String tagName, String value, int indent)
		throws IOException
	{
		// skip if null
		if (FormatUtility.isEmptyOrNull(value))
			return;
		indent(serializer, indent);
		serializer.startTag(NAMESPACE, tagName);
		serializer.text(value);
		serializer.endTag(NAMESPACE, tagName);
		serializer.ignorableWhitespace(NEW_LINE);
	}
	
	private void indent(XmlSerializer serializer, int count)
		throws IllegalArgumentException, IllegalStateException, IOException
	{
		for (int i = 0; i < count; i++)
		{
			serializer.ignorableWhitespace(INDENT);
		}
	}
}
